class Tetris
{
  /* Reference to the only instance of this class */
  static var app : Tetris;
 
  /* Field size.  Must be 300x600 to fit in my artwork */
  static var FIELD_WIDTH : Number = 10;
  static var FIELD_HEIGHT : Number = 20;
  static var BLOCK_SIZE : Number = 30;
 
  /* Delay before first key repeat */
  static var REPEAT_DELAY : Number = 0.2;

  /* Delay between subseqeuent key repeats */
  static var REPEAT_INTERDELAY : Number = 0.1;

  /* Number of seconds elapsed in this round */
  static var t : Number = 0;

  /* Last time the current piece was moved down */
  static var last_update : Number = 0;

  /* The index of the current piece layout */
  static var piece : Number = 0;

  /* The index of the next piece layout.  This is displayed in a box outside
   * the field */
  static var nextPiece : Number = 0;

  /* Rotation of the current piece */
  static var pieceRot : Number = 0;

  /* X and Y position of the current piece */
  static var pieceX : Number = 0;
  static var pieceY : Number = 0;

  /* The contents of the current field.  One Number per cell */
  static var field : Array = new Array();

  /* The colors of the difference pieces */
  static var palette : Array = new Array();

  /* The shape of the difference pieces, in all their orientations */
  static var shapes : Array = new Array();

  /* Number of lines removed.  Used to determine game speed */
  static var lines : Number = 0;

  /* Number of points scored */
  static var score : Number = 0;

  /* Number of seconds between each time the current piece is moved down */
  static var delay : Number = 0.5;

  /* Are we currently dead? */
  static var dead : Boolean = true;

  /* How much of the area has been filled with boxes after death? */
  static var dead_fill : Number = 0;

  /* The player's name, as entered by the player */
  static var name : String = "";

  /* Font information */
  static var number_format : TextFormat = new TextFormat();
  static var text_format : TextFormat = new TextFormat();

  /* When a line is removed, all the boxes on the line are converted into
   * projectiles.  This array contains those projectiles. */
  static var projectiles : Array = new Array();

  /* What keys are currently pressed? */
  static var leftDown : Boolean;
  static var rightDown : Boolean;
  static var downDown : Boolean;
  static var upDown : Boolean;
  static var spaceDown : Boolean;

  /* The time each key was previously removed.  Flash does not provide any clue
   * about what keyboard events are repeat events, so we assume that a keypress
   * that quickly follows a key release is a repeat, and can be ignored. */
  static var left_released : Number;
  static var right_released : Number;
  static var down_released : Number;
  static var up_released : Number;
  static var space_released : Number;

  /* The time of our next key repeat event, as generated by ourselves (not the
   * operating system). */
  static var next_repeat : Number;

  /**
   * The class constructor.  Called once on startup, by the main() function.
   */
  function Tetris()
  {
    var x : Number = 0;

    /* Asynchronously fetch variables into the _root. scope from the specified
     * URL.  In this particular case, the sergver will return a snippet that
     * assigns some text to the _root.score_info variable.  Very nifty! 
     * Go ahead and fetch this URL if you want to see what it looks like. */
    _root.loadVariables("http://www.junoplay.com/tetris-submit");

    /* Set up callbacks for all the relevant events. */
    _root.onEnterFrame = on_enter_frame;
    _root.onMouseUp = on_mouse_up;
    _root.onKeyDown = on_key_down;
    _root.onKeyUp = on_key_up;

    /* We want to receive key events regardless of focus */
    Key.addListener(_root);

    /* The colors of the 7 different tetrominoes */
    palette = [ 0xffcf7f, 0xff7fcf, 0x7fffcf, 0xcfff7f, 0x7fcfff, 0xcf7fff, 0xcfcfcf ];

    shapes =
    [
      [ [ [ 0,0,1,0 ],
          [ 0,0,1,0 ],
          [ 0,0,1,0 ],
          [ 0,0,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 1,1,1,1 ],
          [ 0,0,0,0 ],
          [ 0,0,0,0 ] ],
        [ [ 0,0,1,0 ],
          [ 0,0,1,0 ],
          [ 0,0,1,0 ],
          [ 0,0,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 1,1,1,1 ],
          [ 0,0,0,0 ],
          [ 0,0,0,0 ] ] ],
      [ [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,1,0 ],
          [ 0,1,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,1,0 ],
          [ 0,1,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,1,0 ],
          [ 0,1,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,1,0 ],
          [ 0,1,1,0 ] ] ],
      [ [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,1,1 ],
          [ 0,0,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,1,0 ],
          [ 0,1,1,0 ],
          [ 0,0,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,0,1,0 ],
          [ 0,1,1,1 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,1,0 ],
          [ 0,0,1,1 ],
          [ 0,0,1,0 ] ] ],
      [ [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,1,1 ],
          [ 0,1,0,0 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,1,1 ],
          [ 0,0,0,1 ],
          [ 0,0,0,1 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,0,0,1 ],
          [ 0,1,1,1 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,1,0 ],
          [ 0,0,1,0 ],
          [ 0,0,1,1 ] ] ],
      [ [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,1,1 ],
          [ 0,0,0,1 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,1 ],
          [ 0,0,0,1 ],
          [ 0,0,1,1 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,0,0 ],
          [ 0,1,1,1 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,1,1 ],
          [ 0,0,1,0 ],
          [ 0,0,1,0 ] ] ],
      [ [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,0,1,1 ],
          [ 0,1,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,1,0 ],
          [ 0,0,1,1 ],
          [ 0,0,0,1 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,0,1,1 ],
          [ 0,1,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,1,0 ],
          [ 0,0,1,1 ],
          [ 0,0,0,1 ] ] ],
      [ [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,1,0 ],
          [ 0,0,1,1 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,1 ],
          [ 0,0,1,1 ],
          [ 0,0,1,0 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,0 ],
          [ 0,1,1,0 ],
          [ 0,0,1,1 ] ],
        [ [ 0,0,0,0 ],
          [ 0,0,0,1 ],
          [ 0,0,1,1 ],
          [ 0,0,1,0 ] ] ]
    ];

    /* This is the layer we will be painting to in our frame callback function.
     * I have given it a depth of 10, so it will be above most other things */
    _root.createEmptyMovieClip("lines", 10 /* Depth */);

    /* Place the text fields */
    _root.createTextField("score_field", 2, 340, 33, 425, 40);
    _root.createTextField("high_score", 3, 340, 265, 425, 280);

    /* The start button.  It has a depth of 11, one more than the depth of the
     * painting layer */
    _root.attachMovie("start", "start", 11);
    _root.start._x = 118;
    _root.start._y = 268;

    /* Font used for high score list */
    text_format.font = "DejaVuSansMono";
    text_format.color = 0xffffff;
    text_format.size = 20;
    text_format.align = "left";

    /* Font used for score view */
    number_format.font = "DejaVuSansMono";
    number_format.color = 0xffffff;
    number_format.size = 30;
    number_format.align = "right";

    /* Assign the fonts to the text fields */

    _root.score_field.embedFonts = true;
    _root.score_field.setTextFormat(number_format);
    _root.score_field.setNewTextFormat(number_format);

    _root.high_score.embedFonts = true;
    _root.high_score.setTextFormat(text_format);
    _root.high_score.setNewTextFormat(text_format);

    /* Fill the entire field with boxes */
    for(x = 0; x < FIELD_WIDTH * FIELD_HEIGHT; ++x)
      field[x] = 7;
  }

  /**
   * Start a new game.  Reset all game variables to their initial state.
   */
  static function reset_field() : Void
  {
    var x : Number = 0;

    for(x = 0; x < FIELD_WIDTH * FIELD_HEIGHT; ++x)
      field[x] = 0;

    pieceX = FIELD_WIDTH / 2 - 2;
    pieceY = 0;
    piece = random(7);
    nextPiece = random(7);

    t = 0;
    last_update = 0;

    lines = 0;
    score = 0;
    delay = 0.5;
    dead = false;

    leftDown = false;
    rightDown = false;
    downDown = false;
    upDown = false;
    spaceDown = false;

    left_released = 0;
    right_released = 0;
    down_released = 0;
    up_released = 0;
    space_released = 0;
  }

  /**
   * Check whether any lines have been completely filled.  Convert any filled
   * line into projectiles.
   */
  static function check_lines() : Void
  {
    var x : Number = 0;
    var y : Number = 0;
    var hit : Boolean = false;
    var power : Number = 1000.0;
    var y2 : Number;
    var linesNow : Number = 0;

    for(y = 0; y < FIELD_HEIGHT; ++y)
    {
      for(x = 0; x < FIELD_WIDTH; ++x)
        if(field[y * FIELD_WIDTH + x] == 0)
          break;

      if(x != FIELD_WIDTH)
        continue;

      hit = true;

      for(x = 0; x < FIELD_WIDTH; ++x)
      {
        var p : Projectile = new Projectile();
        var len : Number;

        p.x = x * BLOCK_SIZE;
        p.y = y * BLOCK_SIZE;
        p.vx = random(17) / 16.0 - 0.5;
        p.vy = -random(17) / 16.0 - 0.1;
        p.color = field[y * FIELD_WIDTH + x] - 1;

        len = 1.0 / Math.sqrt((p.vx * p.vx) + (p.vy * p.vy));
        len *= power;
        p.vx *= len;
        p.vy *= len;

        projectiles.push(p);
      }

      power *= 1.3;
      ++linesNow;

      for(y2 = y; y2 > 0; --y2)
        for(x = 0; x < FIELD_WIDTH; ++x)
          field[y2 * FIELD_WIDTH + x] = field[(y2 - 1) * FIELD_WIDTH + x];

      ++lines;

      delay = 0.1 + (0.4 / (lines * 0.03 + 1));

      for(x = 0; x < FIELD_WIDTH; ++x)
        field[x] = 0;
    }

    score += linesNow * linesNow * 357;
  }

  /**
   * Make a new piece appear at the top, or die if there's no room for it.
   */
  static function spawn() : Void
  {
    var x : Number = 0;
    var y : Number = 0;

    for(y = 0; y < 4; ++y)
      for(x = 0; x < 4; ++x)
        if(shapes[piece][pieceRot][y][x])
          field[(pieceY + y) * FIELD_WIDTH + pieceX + x] = piece + 1;

    check_lines();

    piece = nextPiece;
    nextPiece = random(7);
    pieceX = FIELD_WIDTH / 2 - 2;
    pieceY = 0;
    pieceRot = 0;

    if(will_crash(piece, pieceX, pieceY, pieceRot))
    {
      dead = true;
      dead_fill = 0;
      _root.attachMovie("start", "start", 11);
      _root.start._x = 118;
      _root.start._y = 268;

      if(score > 0)
      {
        if(_root.userid)
        {
          _root.score = score;
          _root.loadVariables("http://www.junoplay.com/tetris-submit", "POST");
        }
        else
        {
          _root.score_info = "";
          _root.score = 0;
          _root.high_score.text = "Your name:\n" + name + "_\n\n(Press Enter when finished)";
        }
      }
    }
  }

  /**
   * Move the current piece one unit down.
   */
  static function down() : Boolean
  {
    last_update = t;

    if(will_crash(piece, pieceX, pieceY + 1, pieceRot))
    {
      spawn();

      return true;
    }
    else
      ++pieceY;

    return false;
  }

  /**
   * Test whether a piece will crash in a new position/orientation.  This is
   * useful for determining when a piece has fallen to the bottom, whether a
   * piece is impossible to rotate, and whether a new piece cannot be spawned.
   */
  static function will_crash(piece : Number, px : Number, py : Number, pr : Number) : Boolean
  {
    var x : Number = 0;
    var y : Number = 0;

    for(y = 0; y < 4; ++y)
    {
      for(x = 0; x < 4; ++x)
      {
        if(shapes[piece][pr][y][x]
            && (px + x < 0 || px + x > FIELD_WIDTH
                || py + y < 0 || py + y > FIELD_HEIGHT
                || field[(py + y) * FIELD_WIDTH + px + x]))
        {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Draw a loose shape.  Used to draw the current piece and the next piece.
   */
  static function draw_shape(piece : Number, px : Number, py : Number, pr : Number)
  {
    var x : Number = 0;
    var y : Number = 0;
    var alphas : Array = [ 100, 100 ];
    var ratios : Array = [ 0, 0xFF ];
    var colors, left, top, matrix, color;

    for(y = 0; y < 4; ++y)
    {
      for(x = 0; x < 4; ++x)
      {
        if(shapes[piece][pr][y][x] != 0)
        {
          left = px + x * BLOCK_SIZE;
          top = py + y  * BLOCK_SIZE;

          color = palette[piece];
          colors = [ color, ((color >> 1) & 0x7f7f7f) ];
          matrix =
          {
            matrixType:"box", x:left, y:top, w:BLOCK_SIZE, h:BLOCK_SIZE, r:(45/180) * Math.PI
          };

          _root.lines.moveTo(left,      top);
          _root.lines.lineStyle(2, (color >> 1) & 0x7f7f7f, 100);
          _root.lines.beginGradientFill("linear", colors, alphas, ratios, matrix );
          _root.lines.lineTo(left + (BLOCK_SIZE - 1), top); 
          _root.lines.lineTo(left + (BLOCK_SIZE - 1), top + (BLOCK_SIZE - 1));
          _root.lines.lineTo(left,                    top + (BLOCK_SIZE - 1));
          _root.lines.lineTo(left,                    top);
          _root.lines.endFill();
        }
      }
    }
  }

  /**
   * The function called by the frame loop.
   */
  function on_enter_frame() : Void
  {
    var x : Number = 0;
    var y : Number = 0;

    t += 0.05;

    _root.score_field.text = String(score);

    if(!dead)
    {
      if(t - last_update > delay)
        down();

      if(t > next_repeat)
      {
        if(leftDown)
        {
          if(!will_crash(piece, pieceX - 1, pieceY, pieceRot))
            --pieceX;
        }

        if(rightDown)
        {
          if(!will_crash(piece, pieceX + 1, pieceY, pieceRot))
            ++pieceX;
        }

        if(upDown)
        {
          if(!will_crash(piece, pieceX, pieceY, (pieceRot + 1) % 4))
            pieceRot = (pieceRot + 1) % 4;
        }

        if(downDown)
          down();

        next_repeat = t + REPEAT_INTERDELAY;
      }
    }
    else
    {
      if(dead_fill < FIELD_HEIGHT)
      {
        ++dead_fill;

        for(x = 0; x < FIELD_WIDTH; ++x)
          field[(FIELD_HEIGHT - dead_fill) * FIELD_WIDTH + x] = 7;
      }
    }

    if(_root.score_info.length)
    {
      _root.high_score.text = _root.score_info;
    }

    _root.lines.clear();

    var alphas : Array = [ 100, 100 ];
    var ratios : Array = [ 0, 0xFF ];
    var pc, colors, left, top, matrix, color;

    for(y = 0; y < FIELD_HEIGHT; ++y)
    {
      for(x = 0; x < FIELD_WIDTH; ++x)
      {
        pc = field[y * FIELD_WIDTH + x];

        if(pc > 0)
        {
          left = x * BLOCK_SIZE;
          top = y * BLOCK_SIZE;
          
          color = palette[pc - 1];
          colors = [ color, ((color >> 1) & 0x7f7f7f) ];
          matrix =
          {
            matrixType:"box", x:left, y:top, w:BLOCK_SIZE, h:BLOCK_SIZE, r:(45/180) * Math.PI
          };

          _root.lines.moveTo(left, top);
          _root.lines.lineStyle(2, (color >> 1) & 0x7f7f7f, 100);
          _root.lines.beginGradientFill("linear", colors, alphas, ratios, matrix );
          _root.lines.lineTo(left + (BLOCK_SIZE - 1), top); 
          _root.lines.lineTo(left + (BLOCK_SIZE - 1), top + (BLOCK_SIZE - 1));
          _root.lines.lineTo(left,                    top + (BLOCK_SIZE - 1));
          _root.lines.lineTo(left,                    top);
          _root.lines.endFill();
        }
      }
    }

    x = 0;

    while(x < projectiles.length)
    {
      var p : Projectile = projectiles[x];

      p.x += p.vx * 0.05;
      p.y += p.vy * 0.05;

      if(p.y > 600)
      {
        projectiles.splice(x, 1);    

        continue;
      }

      p.vy += 250.0;

      left = p.x;
      top = p.y;

      color = palette[p.color];
      colors = [ color, ((color >> 1) & 0x7f7f7f) ];
      matrix =
      {
        matrixType:"box", x:left, y:top, w:BLOCK_SIZE, h:BLOCK_SIZE, r:(45/180) * Math.PI
      };

      _root.lines.moveTo(left, top);
      _root.lines.lineStyle(2, (color >> 1) & 0x7f7f7f, 100);
      _root.lines.beginGradientFill("linear", colors, alphas, ratios, matrix);
      _root.lines.lineTo(left + (BLOCK_SIZE - 1), top); 
      _root.lines.lineTo(left + (BLOCK_SIZE - 1), top + (BLOCK_SIZE - 1));
      _root.lines.lineTo(left,                    top + (BLOCK_SIZE - 1));
      _root.lines.lineTo(left,                    top);
      _root.lines.endFill();

      ++x;
    }

    if(!dead)
    {
      draw_shape(piece, pieceX * BLOCK_SIZE, pieceY * BLOCK_SIZE, pieceRot);
      draw_shape(nextPiece, 330, 105, 0);
    }
  }

  /**
   * The player pressed a key on his keyboard.
   */
  function on_key_down() : Void
  {
    var k : Number = Key.getCode();

    if(dead)
    {
      if(score > 0 && !_root.score)
      {
        switch(k)
        {
        case Key.BACKSPACE:

          if(name.length > 0)
            name = name.slice(0, name.length - 1);

          break;

        case Key.SPACE:

          if(name.length > 0)
            name = name + " ";

          break;

        case Key.ENTER:

          if(!_root.score && name.length)
          {
            _root.name = name;
            _root.score = score;
            _root.loadVariables("http://www.junoplay.com/tetris-submit", "POST");
            _root.high_score.text = "Score submitted";
          }

          break;

        case Key.TAB:

          break;

        default:

          if(Key.getAscii() && name.length < 16)
            name = name + String.fromCharCode(Key.getAscii());
        }

        if(!_root.score)
        {
          _root.high_score.text = "Your name:\n" + name + "_\n\n(Press Enter when finished)";
        }
      }

      return;
    }

    switch(k)
    {
    case Key.RIGHT:

      if(!rightDown)
      {
        rightDown = true;

        if(t - right_released > 0.01)
        {
          if(!will_crash(piece, pieceX + 1, pieceY, pieceRot))
            ++pieceX;

          next_repeat = t + REPEAT_DELAY;
        }
      }

      break;

    case Key.LEFT:

      if(!leftDown)
      {
        leftDown = true;

        if(t - left_released > 0.01)
        {
          if(!will_crash(piece, pieceX - 1, pieceY, pieceRot))
            --pieceX;

          next_repeat = t + REPEAT_DELAY;
        }
      }

      break;

    case Key.UP:

      if(!upDown)
      {
        upDown = true;

        if(t - up_released > 0.01)
        {
          if(!will_crash(piece, pieceX, pieceY, (pieceRot + 1) % 4))
            pieceRot = (pieceRot + 1) % 4;

          next_repeat = t + REPEAT_DELAY;
        }
      }

      break;

    case Key.SPACE:

      if(!spaceDown)
      {
        spaceDown = true;

        if(t - space_released > 0.01)
        {
          while(!down())
            score += 7;
        }
      }

      break;

    case Key.DOWN:

      if(!downDown)
      {
        downDown = true;

        if(t - down_released > 0.01)
        {
          down();

          next_repeat = t + REPEAT_DELAY;
        }
      }

      break;
    }
  }

  /**
   * The player released a key on his keyboard.
   */
  function on_key_up() : Void
  {
    var k : Number = Key.getCode();

    switch(k)
    {
    case Key.RIGHT:

      right_released = t;
      rightDown = false;

      break;

    case Key.LEFT:

      left_released = t;
      leftDown = false;

    case Key.UP:

      up_released = t;
      upDown = false;

      break;

    case Key.SPACE:

      space_released = t;
      spaceDown = false;

      break;

    case Key.DOWN:

      down_released = t;
      downDown = false;

      break;
    }
  }

  /**
   * A mouse button was released.
   */
  function on_mouse_up() : Void
  {
    if(_root.start.hitTest(_root._xmouse, _root._ymouse, false))
    {
      reset_field();
      _root.start.removeMovieClip();

      if(!_root.score_info.length)
        _root.loadVariables("http://www.junoplay.com/tetris-submit");
    }
  }

  /**
   * The entry point, as defined in tetris.xml:
   *
   *   <call object="Tetris" method="main" />
   */
  static function main() : Void
  {
    app = new Tetris();
  }
}
